Integrantes: Víctor Caro, Víctor Navarro, Javier Gómez
Profesora: Dra. Bárbara Poblete
Auxiliares: Hernán Sarmiento, José Miguel Herrera
Fecha de Entrega: 21-12-2018
En el presente documento se expone la totalidad del trabajo realizado para el proyecto semestral del curso CC5206-Introducción a la Minería de Datos.
Para el hito 2, se reformuló el hito 1 considerando los comentarios de la profesora. Se revisitó el problema con una perspectiva distinta, se buscó un objetivo aparte al presentado por la competencia de Kaggle y se escribió el código por cuenta propia en python en vez de ocupar lo presentado en el informe anterior, donde se usó un tutorial en R como pauta a seguir. Cabe destacar que se usó como inspiración lo entregado en la tarea Informe Hito 1.
En el hito 3 se creó un clasificador de barrios a partir de las casas en el dataset, incluido su precio de venta. Para esta tarea se ocupó SVM y Random Forest. Adicionalmente, con el objetivo en mente de crear un recomendador de características relevantes para casas se aplicó Nonnegative Matrix Factorization (NMF) al dataset considerando 45 atributos generalizables dentro de la totalidad de atributos del dataset. El objetivo de aplicar NMF es la clusterización automática de las columnas del dataset en un número de grupos aribitrario, los cuales están determinados por los pesos de las características que lo componen. De esta manera, estos grupos se pueden interpretar como "Tipos de casas", donde un tipo de casa se diferencia de otro en los pesos de las características que lo componen. Así, se creó una matriz H de casas y grupos de casa, donde el valor en una celda i,j es el peso que posee una determinada casa en el grupo i. Luego, una casa pertenece a un grupo si posee un valor en dicha celda superior a un umbral arbitrario, modelando así un comportamiento más realista donde una casa no sólo pertenece exclusivamente a un cluster sino puede pertenecer una agrupación de estos.
Crear un recomendador a partir de esto es complejo, porque no hay manera de determinar qué tipo de casa es mejor para todos, por lo que a partir de los grupos generados se pueden obtener las 10 características más relevantes por grupo de casa, así, si se puede establecer una relación entre un cierto número de tipos de casas y los grupos generados a partir de NMF, podría uno escoger una casa, determinar que pertenece a un cierto grupo y a partir de esto escoger las 10 características más relevantes en las que uno debería fijarse al momento de vender.
La valuación de un bien inmueble es el proceso de desarrollar una opinión de valor para el bien en cuestión. La transacción de un bien inmueble requiere una tasación previa para determinar su valor. Múltiples factores influyen en la valoración final de un bien, encontrando entre estos su vecindario, tamaño, materiales usados para su construcción, cantidad de habitaciones, remodelaciones, etcétera. Es de interés para cualquier persona que desee participar en alguna transacción de bienes inmuebles tener una buena tasación previa del bien a transaccionar.
Nuestra propuesta de proyecto semestral para el curso CC5206 - Introducción a la Minería de Datos consiste en analizar el Ames Housing dataset, compilado por Dean De Cock y publicado en Kaggle 2 como una competencia para principiantes. Este dataset contiene registros de casas vendidas entre 2006 y 2010 en Ames, Iowa, con una serie de características y su valoración en USD.
De esta manera, crear un predictor del valor de una porpiedad, permitiría ahorrar el trabajo del tasador. Otro punto de estos datos es que permite analizar qué atributos influyen más o menos, si la fecha de venta influye o no, la estación del año, etc.
El objetivo principal de nuestro proyecto es predecir el precio final de una casa a partir de sus características. Este trabajo actualmente está a cargo de un tasador y nos gustaría explorar qué tan cerca de los precios finales de un tasador están las predicciones de algoritmos aplicados a nuestros datos. A raíz de esto surgen una serie de preguntas tales como ¿Cuál es la distribución de precios de venta de las casas en el dataset?¿Cómo es la condición general de las casas vendidas?¿Existen características de una casa que no influyen en el precio de esta?, naturalmente también, ¿Qué características son las más relevantes al tasar una casa?, ¿Qué características de una casa poseen una alta o baja correlación?¿Qué parámetros poseen una baja o alta varianza?¿Cuántas casas han sido remodeladas?¿Con qué parámetros está positivamente correlacionado el precio de venta?¿Y negativamente? Abordamos estas preguntas y otras en el análisis exploratorio a continuación.
Al mismo tiempo, creemos que podría existir interés por entes gubernamentales o privados, los cuales podrían estar interesados en estudiar segmentos de alguna ciudad en particular, o predecir precios de venta de proyectos inmobiliarios. Dada la naturaleza del entrenamiento de los algoritmos, sería necesario realizar un entrenamiento distinto por cada ciudad, puesto que las relaciones existentes en los barrios de una localidad no necesariamente son las mismas o si quiera similares a las de otra. Por lo tanto, buscamos determinar qué factores son generalizables, vale decir, qué características de una casa tienden a ser determinantes al momento de establecer un precio de venta. De esta manera, creemos que nuestro proyecto podría ser relevante para tasadores u otros expertos que deseen buscar apoyo en herramientas computacionales para la realización de su labor.
%matplotlib inline
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="darkgrid")
from sklearn import metrics, cross_validation
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import mean_squared_error, explained_variance_score
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import Imputer
df = pd.read_csv("/content/gdrive/My Drive/train.csv", index_col = 0)
print(df.shape)
df.head()
Vemos que nuestro dataset contiene 1460 filas y 80 columnas, las que corresponden a diversos atributos de las casas. Para una explicación detallada de cada uno de estos, referirse al documento adjunto "data_description.txt"
En vista de que existen columnas con NaNs, buscamos las columnas que tengan más de 200 valores NaN y hacemos un drop en el dataframe. Notar que si mostramos la variable 'nan_por_columnas', las que cumplen con esta condición son las primeras 6.
nan_por_columnas = df.isna().sum().sort_values(axis=0, ascending=False)
top_6 = nan_por_columnas[:6]
df = df.drop(columns=top_6.index.tolist())
print(top_6)
df.shape
Se puede observar que las columnas con mayor cantidad de valores NaNs son:
Con esto podemos ver que la mayor parte de las casas no poseen piscina, características misceláneas (como ascensores o cancha de tenis), callejón, cerca ni chimeneas.
Podemos estudiar la distribución de casas por barrio usando la columna Neighborhood. Notar que los nombres están acotados.
plt.figure(figsize=(8,8))
plt.title('Cantidad de propiedades por barrio', size=20)
sns.countplot(y='Neighborhood', data=df)
plt.xlabel('Cantidad de propiedades')
plt.show()
Hay dos barrios que concentran una mayor cantidad de propiedades, que son North Ames (NAmes) y College Creek (CollgCr).
Ahora consideramos la columna MSZoning, la cual identifica la clasificación general de zonificación de la propiedad. Los códigos utilizados corresponden a zonas Comerciales (C), Pueblo Flotante Residencial(FV), Residencial de Alta Densidad (RH), Residencial de baja densidad (RL) y residencial de mediana densidad (RM).
plt.figure(figsize=(8,8))
plt.title('Distribución de casas por Zonificación', size=20)
sns.countplot(y='MSZoning', data=df)
plt.ylabel('Categorías de Zonificación')
plt.xlabel('Cantidad de propiedades')
plt.show()
Vemos que la mayor parte de las casas están en áreas residenciales de baja densidad, seguidas por mediana densidad.
plt.figure(figsize=(15,8))
sns.boxplot(x="Neighborhood", y="SalePrice", data=df)
plt.xticks(rotation=45)
Podemos estudiar el precio de venta de las casas:
df['SalePrice'].describe()
sns.distplot(df['SalePrice'], bins=50, kde=False)
Dada la alta cantidad de carácterísticas, para graficar la matriz de correlación consideramos solo aquellas que tengan una correlación superior a 0.4 (sea positiva o negativa):
c = df.corr()
m = (c.mask(np.eye(len(c),dtype=bool)).abs()>0.4).any()
corr = c.loc[m,m]
f, ax = plt.subplots(figsize=(10,8))
sns.heatmap(corr, square=True, ax=ax)
Podemos sacar las 6 características más correlacionadas:
c = corr.abs().unstack().sort_values(ascending=False)
c = c[corr.shape[0]:corr.shape[0]+6]
top_6 = []
for i in range(0,6,2):
top_6.append(c.keys()[i][0])
top_6.append(c.keys()[i][1])
print(c)
print(top_6)
Podemos entonces graficar sus densidades:
f = plt.figure(figsize =(15,12))
plt.suptitle('Top 6 Correlaciones', fontsize=16)
for i in range(0,6):
feature = top_6[i]
ax = f.add_subplot(3,3,i+1)
sns.distplot(df[feature].fillna(df[feature].mean()), label=feature)
plt.legend()
plt.show()
Anteriormente se presentó el trabajo correspondiente al hito 1. A continuación se presentan los experimentos realizados y resultados preliminares obtenidos hasta el momento.
Como se puede observar, hay columnas dentro del dataset que contienen strings. $\texttt{scikit-learn}$ no trabaja con strings, por lo que codeamos dichas columnas con un entero para cada string usando LabelEncoder():
#Guardamos la lista de barrios
barrios = df['Neighborhood'].value_counts().keys().tolist()
featureList = np.array(df.columns).tolist()[0:df.shape[1]-1]
for feature in featureList:
if df[feature].dtype == 'object':
df[feature] = LabelEncoder().fit_transform(df[feature].tolist())
df.head()
De acuerdo a la distribución de precios, determinamos que las viviendas con precios superiorer a los 500000 USD son considerados outliers. Por lo tanto son eliminados del dataset:
print('Dimensiones antes de sacar outliers:',df.shape)
df_drop = df.drop(df[df['SalePrice']>500000].index)
print('Dimensiones después de sacar outliers:',df_drop.shape)
Otra cosa a mencionar es que los datos NaN deben tener valores finitos. Para evitar overfitting se prefiere dejar dichos valores como el promedio. Para ello se ocupa Imputer():
imp = Imputer(missing_values=np.nan, strategy='mean')
df_drop[featureList] = imp.fit_transform(df_drop[featureList])
Escalamos todos los datos usando RobustScaler, el cual usa métodos robustos en caso de outliers en las columnas:
from sklearn.preprocessing import RobustScaler
df_drop[featureList] = RobustScaler().fit_transform(df_drop[featureList])
Para predecir los precios por barrio separamos el dataset:
df_barrios = {}
barrios_num = df_drop['Neighborhood'].value_counts().keys()
for i in range(0,len(barrios_num)):
df_barrios[barrios[i]] = df_drop[df_drop['Neighborhood'] == barrios_num[i]]
df_barrios[barrios[i]] = df_barrios[barrios[i]].drop(columns=['Neighborhood'])
print('Cantidad de barrios:',len(df_barrios.keys()))
Descartamos los barrios con menos de 90 viviendas:
for key, df in list(df_barrios.items()):
if(df.shape[0]<=90):
df_barrios.pop(key,None)
print('Cantidad de barrios:',len(df_barrios.keys()))
GradientBoostingRegressor es una función de $\texttt{scikit-learn}$, corresponde un modelo por etapas, el cual permite optimizar múltiples loss functions usando un árbol de decisión con una regresión por cada etapa.
Posee 3 importantes parámetros: n_estimator, max_depth, min_samples_split. A priori desconocemos cual combinación de valores es la mejor para obtener el mejor score, por lo que procedemos a usar el método GridSearchCV. Dicho método recibe un clasificador y un diccionario cuyas llaves corresponden a los argumentos del clasificador, cada llave posee un array de posibles valores. GridSearchCV se encarga de obtener la combinación de argumentos que entregan el mejor score para el clasificador.
for key, dfs in df_barrios.items():
print("############## Regresión para", key, "###############")
print("Dimensiones:", dfs.shape)
X = dfs[dfs.columns[:-1]]
y = dfs[dfs.columns[-1]]
clf = GradientBoostingRegressor()
param_grid = {'n_estimators': [100, 200, 300, 400, 500, 600, 700], 'max_depth': [1, 2, 3, 4, 5, 6, 7, 8, 9],
'min_samples_split': [2, 3 ,4 ,5 , 6]}
#n_jobs corresponde a la cantidad de threads a ocupar, cv corresponde a los k-folds a ocupar en la cross-validation
clf_grid = GridSearchCV(clf, param_grid, verbose=1, n_jobs=-1, cv=3)
clf_grid.fit(X, y)
print("Mejores parametros GBR:", clf_grid.best_params_)
print("Score CV: ", clf_grid.best_score_)
Resumidamente un random forest corresponde a un conjunto de árboles de decisión donde cada árbol recibe un subconjunto de las features disponibles del dataset (distinto para cada árbol) para entrenar. El fin de esto es no caer en mínimos locales al entrenar. El algoritmo al final entrega la moda en los resultados que entregó cada árbol.
for key, dfs in df_barrios.items():
print("############## Regresión para", key, "###############")
print("Dimensiones:", dfs.shape)
X = dfs[dfs.columns[:-1]]
y = dfs[dfs.columns[-1]]
clf = RandomForestRegressor()
param_grid = {'n_estimators': [100, 200, 300, 400, 500, 600, 700], 'max_depth': [1, 2, 3, 4, 5, 6, 7, 8, 9],
'min_samples_split': [2, 3 ,4 ,5 , 6]}
#n_jobs corresponde a la cantidad de threads a ocupar, cv corresponde a los k-folds a ocupar en la cross-validation
clf_grid = GridSearchCV(clf, param_grid, verbose=1, n_jobs=-1, cv=3)
clf_grid.fit(X, y)
print("Mejores parametros GBR:", clf_grid.best_params_)
print("Score CV: ", clf_grid.best_score_)
X = df_drop[df_drop.columns[:-1]]
y = df_drop[df_drop.columns[-1]]
clf = GradientBoostingRegressor()
param_grid = {'n_estimators': [100, 200, 300, 400, 500, 600, 700], 'max_depth': [1, 2, 3, 4, 5, 6, 7, 8, 9],
'min_samples_split': [2, 3 ,4 ,5 , 6]}
#n_jobs corresponde a la cantidad de threads a ocupar, cv corresponde a los k-folds a ocupar en la cross-validation
clf_grid = GridSearchCV(clf, param_grid, verbose=1, n_jobs=-1, cv=3)
clf_grid.fit(X, y)
print("Mejores parametros GBR:", clf_grid.best_params_)
print("Score CV: ", clf_grid.best_score_)
clf = RandomForestRegressor()
param_grid = {'n_estimators': [100, 200, 300, 400, 500, 600, 700], 'max_depth': [1, 2, 3, 4, 5, 6, 7, 8, 9],
'min_samples_split': [2, 3 ,4 ,5 , 6]}
#n_jobs corresponde a la cantidad de threads a ocupar, cv corresponde a los k-folds a ocupar en la cross-validation
clf_grid = GridSearchCV(clf, param_grid, verbose=1, n_jobs=-1, cv=3)
clf_grid.fit(X, y)
print("Mejores parametros RFR:", clf_grid.best_params_)
print("Score CV: ", clf_grid.best_score_)
#Mejores parametros GradientBoosting: {'max_depth': 2, 'min_samples_split': 2, 'n_estimators': 500}
#Mejores parametros RandomForest: {'max_depth': 9, 'min_samples_split': 2, 'n_estimators': 300}"
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
clf = RandomForestRegressor(n_estimators=clf_grid.best_params_['n_estimators'],
max_depth=clf_grid.best_params_['max_depth'],
min_samples_split=clf_grid.best_params_['min_samples_split'])
clf.fit(X_train, y_train)
print("Score: ",clf.score(X_test,y_test))
y_pred = clf.predict(X_test)
print("Explained Variance Score:", explained_variance_score(y_test, y_pred)) #mientras mas cercano a 1 mejor
print("MSE: ", mean_squared_error(y_test, y_pred)) #mientras mas cercano a 0 mejor
feature_importance = clf.feature_importances_
# importancia relativa a la caracteristica más importante
feature_importance = 100.0 * (feature_importance / feature_importance.max())
feature_score = {}
df_drop2=df_drop
for i in range(0,len(feature_importance)):
if(feature_importance[i]<0.3): #Saco las de bajo el 0.3%
df_drop2=df_drop2.drop(columns=featureList[i])
df_drop2.shape
X = df_drop2[df_drop2.columns[:-1]]
y = df_drop2[df_drop2.columns[-1]]
clf = GradientBoostingRegressor()
param_grid = {'n_estimators': [100, 200, 300, 400, 500, 600, 700], 'max_depth': [1, 2, 3, 4, 5, 6, 7, 8, 9],
'min_samples_split': [2, 3 ,4 ,5 , 6]}
#n_jobs corresponde a la cantidad de threads a ocupar, cv corresponde a los k-folds a ocupar en la cross-validation
clf_grid = GridSearchCV(clf, param_grid, verbose=1, n_jobs=-1, cv=3)
clf_grid.fit(X, y)
print("Mejores parametros GBR:", clf_grid.best_params_)
print("Score CV: ", clf_grid.best_score_)
clf = RandomForestRegressor()
param_grid = {'n_estimators': [100, 200, 300, 400, 500, 600, 700], 'max_depth': [1, 2, 3, 4, 5, 6, 7, 8, 9],
'min_samples_split': [2, 3 ,4 ,5 , 6]}
#n_jobs corresponde a la cantidad de threads a ocupar, cv corresponde a los k-folds a ocupar en la cross-validation
clf_grid = GridSearchCV(clf, param_grid, verbose=1, n_jobs=-1, cv=3)
clf_grid.fit(X, y)
print("Mejores parametros RFR:", clf_grid.best_params_)
print("Score CV: ", clf_grid.best_score_)
Como fué mencionado en la introducción, el hito 3 tiene como medular la predicción de barrios a partir de sus características y la clusterización de casas en tipos de casas, para luego extraer las características más relevantes por cada grupo generado.
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV, train_test_split, cross_val_predict
from sklearn.model_selection import StratifiedShuffleSplit
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
def print_confusion_matrix(cm, class_names, title="", figsize = (10,7), fontsize=14):
cm = cm / cm.astype('float').sum(axis=1)
df_cm = pd.DataFrame(cm, index=class_names, columns=class_names,)
fig = plt.figure(figsize=figsize)
heatmap = sns.heatmap(df_cm, annot=True, cmap=plt.cm.Blues)
heatmap.yaxis.set_ticklabels(heatmap.yaxis.get_ticklabels(), rotation=0, ha='right', fontsize=fontsize)
heatmap.xaxis.set_ticklabels(heatmap.xaxis.get_ticklabels(), rotation=45, ha='right', fontsize=fontsize)
plt.title(title)
plt.ylabel('True label')
plt.xlabel('Predicted label')
return fig
df = pd.read_csv("train2.csv", index_col = 0)
print("dimensiones del dataset:",df.shape)
#Guardamos los barrios
barrios = df['Neighborhood']
df = df.drop(columns=["Neighborhood"])
featureList = np.array(df.columns).tolist()
# Codeamos todos los strings salvo los barrios
for feature in featureList:
if df[feature].dtype == 'object':
df[feature] = LabelEncoder().fit_transform(df[feature].tolist())
# Llenamos los NaN con la media
imp = Imputer(missing_values=np.nan, strategy='mean')
df[featureList] = imp.fit_transform(df[featureList])
# Escalamos
df[featureList] = RobustScaler().fit_transform(df[featureList])
#Recuperamos los barrios
df["Neighborhood"] = barrios
print(df.shape)
df.head()
Para la clasificación se ocuparán los barrios con un mínimo de 50 viviendas. A continuación se muestran la cantidad de viviendas por barrio.
barrios.value_counts()
barriosOK = [] # barrios que quedan dentro del clasificador
df2 = df
for barrio, v in barrios.value_counts().iteritems():
# Si la cantidad de casas es menor a 50 se descarta
if v < 50:
df2 = df2[df2["Neighborhood"] != barrio]
else:
barriosOK.append(barrio)
Se hace un oversampling de todos los barrios que no alcanzan las 225 viviendas que tiene NAmes, para ello se ocupa la técnica SMOTE que crea datos sintéticos a partir de una muestra usando $\texttt{K-Neighbors}$ con en este caso $k=5$. De esta forma todos los barrios tendrán la misma cantidad de viviendas para hacer la clasificación.
X = df2[df2.columns[:-1]]
y = df2[df2.columns[-1]]
# SMOTE
from imblearn.over_sampling import SMOTE
sm = SMOTE(random_state=42)
X, y = sm.fit_resample(X, y)
Se hace un GridSearchCV en busca de los mejores parámetros para un Random Forest Classifier
cv = StratifiedShuffleSplit(n_splits=5, test_size=0.2, random_state=42)
param_gridRFC = {'n_estimators': np.arange(200,1001,100), 'max_depth': np.arange(15,len(featureList))}
clf_gridRFC = GridSearchCV(RandomForestClassifier(random_state=42), param_gridRFC, verbose=1, n_jobs=-1, cv=cv)
clf_gridRFC.fit(X, y)
print("Mejores parametros RFC: ", clf_gridRFC.best_params_)
print("Score: ", clf_gridRFC.best_score_)
La búsqueda de los mejores parámetros arroja que con $\texttt{max_depth = 18}$ y $\texttt{n_estimators = 600}$ se consigue un score de 0.9077. Pasamos entonces a entrenar Random Forest con un training set del 70%:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.3, random_state=20, stratify=y)
rfc = RandomForestClassifier(n_estimators=600, max_depth=18, random_state=42)
rfc.fit(X_train, y_train)
y_predRFC = rfc.predict(X_test)
print("Accuracy en test set:", accuracy_score(y_test, y_predRFC))
print("Reporte de Clasificacion:")
print(classification_report(y_test, y_predRFC))
De esto podemos sacar la importancia que le dió Random Forest a cada característica. Sacamos entonces todas las características con una importancia relativa a la característica más importante menor al 5%:
X = df2[df2.columns[:-1]]
y = df2[df2.columns[-1]]
feature_importance = rfc.feature_importances_
# importancia relativa a la caracteristica más importante
feature_importance = 100.0 * (feature_importance / feature_importance.max())
X2=X
for i in range(0,len(feature_importance)):
if(feature_importance[i]<5): #Saco las de bajo el 5%
X2=X2.drop(columns=featureList[i])
print("Nuevas dimensiones del dataset:", X2.shape)
Al tener menos características en el dataset se vería afectado el Score y el tiempo de ejecución de los métodos. Por lo que ejecutamos Random Forest nuevamente aplicandole el algorítmo SMOTE al dataset:
X_train, X_test, y_train, y_test = train_test_split(X2, y, test_size=.3, random_state=20, stratify=y)
#SMOTE
sm = SMOTE(random_state=42)
X_train, y_train = sm.fit_resample(X_train, y_train)
X2 = X_train
y = y_train
X2, y = sm.fit_resample(X2, y)
X2.shape
# Tarda bastante, no correr
cv = StratifiedShuffleSplit(n_splits=5, test_size=0.2, random_state=42)
param_gridRFC = {'n_estimators': np.arange(200,1001,100), 'max_depth': np.arange(15,len(featureList))}
clf_gridRFC = GridSearchCV(RandomForestClassifier(random_state=42), param_gridRFC, verbose=1, n_jobs=-1, cv=cv)
clf_gridRFC.fit(X2, y)
print("Mejores parametros RFC: ", clf_gridRFC.best_params_)
print("Score: ", clf_gridRFC.best_score_)
Esta búsqueda de los mejores parámetros arroja que con $\texttt{max_depth = 21}$ y $\texttt{n_estimators = 600}$ se consigue un score de 0.9059, por lo que la disminución de características no afectó mucho el Score. Pasamos entonces a entrenar Random Forest con un training set del 70% y obtenemos su matriz de confusión:
X_train, X_test, y_train, y_test = train_test_split(X2, y, test_size=.3, random_state=20, stratify=y)
rfc = RandomForestClassifier(n_estimators=600, max_depth=21, random_state=42)
rfc.fit(X_train, y_train)
y_predRFC = rfc.predict(X_test)
conf_matrixRFC = confusion_matrix(y_test, y_predRFC)
print("Accuracy en test set:", accuracy_score(y_test, y_predRFC))
print("Reporte de Clasificacion:")
print(classification_report(y_test, y_predRFC))
classNames = barriosOK
f = print_confusion_matrix(conf_matrixRFC, classNames, title='Confusion Matrix Random Forest')
plt.show()
Adicionalmente podemos hacer Suppport Vector Machine, buscamos los mejores parámetros con GridSearchCV:
param_gridSVM = {'C': [1, 10 , 100, 1000], 'gamma': [0.001,0.1, 1, 10], 'kernel': ['linear', 'rbf'],}
clf_gridSVM = GridSearchCV(SVC(), param_gridSVM, verbose=1, n_jobs=-1, cv=cv)
clf_gridSVM.fit(X2, y)
print("Mejores parametros SVM: ", clf_gridSVM.best_params_)
print("Score: ", clf_gridSVM.best_score_)
print("Mejores estimadores SVM: ", clf_gridSVM.best_estimator_)
Lo que arroja que los mejores parámetros serían $C=10$, $\gamma=0.1$ y $\texttt{kernel = 'rbf'}$ con un score del 0.889. Pasamos entonces a entrenar con un training set del 70% y obtenemos la matríz de confusión:
svm = SVC(C=10, gamma=0.1, kernel='rbf')
svm.fit(X_train, y_train)
y_predSVM = svm.predict(X_test)
conf_matrixSVM = confusion_matrix(y_test, y_predSVM)
print("Accuracy en test set:", accuracy_score(y_test, y_predSVM))
print("Reporte de Clasificacion:")
print(classification_report(y_test, y_predSVM))
classNames = barriosOK
f = print_confusion_matrix(conf_matrixSVM, classNames, title='Confusion Matrix SVM')
plt.show()
De las dos matrices de confusión se puede ver Sawyer es mejor clasificado en SVM que en Random Forest, mientras que CollgCr y Edwards son mejores clasificados en Random Forest. En cuando a Gilbert se muestra una deficiencia de ambos algorítmos en su clasificación, esto podría ser porque en Gilbert podrían abundar viviendas muy distintas y considerando que se hizo un SMOTE de 79 a 225 viviendas se podría haber aumentado el Bias de este.
from sklearn.decomposition import NMF
df = pd.read_csv('train.csv', index_col = 0)
Seleccionamos a mano atributos que consideramos generalizables a otros datasets y podrían determinar de mejor manera a distintos tipos de casas:
atributos = ["LotFrontage","LotArea", "OverallQual","OverallCond","MasVnrArea", "ExterQual","ExterQual", "BsmtQual","BsmtCond","BsmtExposure","BsmtFinType1","BsmtFinSF1","BsmtFinType2",
"BsmtFinSF2","TotalBsmtSF","BsmtUnfSF","HeatingQC","CentralAir","1stFlrSF","2ndFlrSF","LowQualFinSF",
"GrLivArea","BsmtFullBath","BsmtHalfBath","FullBath","HalfBath","BedroomAbvGr","KitchenAbvGr","KitchenQual",
"TotRmsAbvGrd","Fireplaces","FireplaceQu","GarageArea","GarageQual",
"GarageCond","WoodDeckSF","OpenPorchSF","EnclosedPorch","3SsnPorch","PoolArea", "ScreenPorch","PoolQC",
"MoSold","MiscVal", "SalePrice"] #
featureList = np.array(df.columns).tolist()
for feature in featureList:
if df[feature].dtype == 'object':
df[feature] = LabelEncoder().fit_transform(df[feature].tolist())
imp = Imputer(missing_values=np.nan, strategy='mean')
df[featureList] = imp.fit_transform(df[featureList])
trainNMF = df.filter(atributos)
Tomamos la matriz transpuesta de trainNMF para entrenar de manera apropiada. Ahora, las filas corresponden a las casas y las columnas a los atributos:
trainNMF = trainNMF.transpose()
trainNMF.head()
Generamos estadísticas para distintos números de grupos de casas a generar:
for i in range(2,11):
print("STATISTICS FOR ", i, "GROUPS")
model = NMF(n_components = i, solver = 'cd' )
W = model.fit_transform(trainNMF)
H = model.components_
for j in range(len(H)):
print("The standard deviation of group ", j, "is: ", H[j].std())
print("The mean of group ", j, "is: ", H[j].mean())
print("The 75th percentile of group ",j,"is: ", np.percentile(H[j], 75))
print("The min of this group is: ", H[j].min(), "and the max is: ", H[j].max())
print("----------------------------------------------------------------")
Escogemos trabajar con 7 grupos de casas distintos:
model = NMF(n_components = 7, solver = 'cd' )
W = model.fit_transform(trainNMF)
H = model.components_
A partir del percentil de los valores en cada grupo de la matriz H, determinamos de manera arbitraria que si una casa posee un valor mayor al percentil 75 dentro de un grupo, entonces esa casa pertenece a tal grupo. De esta manera, no pueden quedar grupos vacíos ya que se determina la pertenencia en función al percentil determinado por las mismas casas, y una casa puede pertenecer a uno o más grupos dependiendo de su peso en cada uno de los grupos.
clusterized_houses = [[] for _ in range(len(H))]
for i in range(len(H)):
threshold = np.percentile(H[i], 75)
# print(threshold)
for j in range(1460):
if H[i][j] >= threshold:
clusterized_houses[i].append(1)
else:
clusterized_houses[i].append(0)
for i in range(len(H)):
n = sum(element > 0 for element in H[i])
print("La cantidad de casas en el grupo ",i,"es: ",n)
¿Cómo podemos asignar una característica de una casa a uno de estos 7 grupos? Intuitivamente, si una característica de casa tiene un peso largo en un tipo de casa, se debería asignar esta característica a ese tipo de casa. Por ejemplo, si en el grupo 1 (vale decir, tipo de casa 1) el peso de OverallCond (condición general de la casa) es muy grande, deberíamos asignar esta característica como perteneciente a este grupo/cluster de tipos de casas.
Una misma característica puede pertenecer a más de un grupo, lo cual es esperable dado que por muy distintas que sean las casas una que otra característica van a tener en común. Bajo esta estrategia de clustering, cada característica de casa se asigna a múltiples clústeres o ningún grupo. Para determinar qué es muy grande podemos ocupar el p-percentile como umbral (si una característica tiene un peso mayor al umbral en un cierto grupo, la asignamos a un grupo).
Luego, cada uno de los 7 grupos que generamos de manera arbitraria está determinado por las características que lo componen. E.g., grupo 1 = {OpenPorchSF, LotFrontage,...,SalePrice}, grupo 2 = {FirePlaces, HeatingQC,...,LotArea}, etcétera.
Para cada uno de los 7 grupos generados, podemos escoger las 10 características más importantes que lo definen. La idea detrás de esto es que si de alguna manera podemos encontrar una relación entre grupos de casas definidos por expertos y los grupos de casas generados por nuestro algoritmo, luego un asesor que esté interesado en vender una casa puede estimar qué tipo de casa está viendo y gracias a la relación encontrada, determinar las 10 características más relevantes para esa casa en particular, y así dar alguna recomendación a las personas interesadas en la transacción.
tr = W.transpose()
for i in range(len(tr)):
aux = []
for element in tr[i].argsort()[-10:]:
aux.append(atributos[element])
print("Las 10 características más relevantes para el grupo ", i+1, "son: ",aux)
Luego de realizar los experimentos, podemos mencionar en relación a nuestro objetivo principal, predecir el precio de venta de una casa a partir de sus atributos, que Gradient Boostin Regression entrega mejores resultados (GBR R^2 Score: 0.903) que Random Forest Regression (R^2 Score: 0.869). Al reducir la dimensionalidad eliminando features estas métricas se mantuvieron relativamente constante (score R^2 CBR = 0.896 vs score R^2 RFR = 0.868), mientras que el tiempo de entrenamiento se redujo en alrededor de un 30%, presentandose así una buena opción para entrenar con recursos limitados y/o mayor cantidad de datos, al mismo tiempo habla sobre la redundancia de más de la mitad de los parámetros, ya que las columnas utilizadas bajaron de 74 a 34 sin perjudicar el desempeño de los algoritmos.
Otro punto relevante es que nuestro análisis de casas por barrios es débil y el motivo de esto fué ignorado hasta muy tarde en el desarrollo de nuestro proyecto. La forma en la que predecimos precio por casa en función del barrio es la misma que ocupamos para predecir precio de casas a partir de sus atributos en general, sólo que en vez de ocupar el dataset completo se reducen las filas a cantidades muy pequeñas, lo cual no presenta mayor interés ni relevancia de acuerdo a lo que esperamos producir y obtener. A partir de esto creemos que es interesante entrenar un clasificador que a partir de las características de una casa, incluyendo su precio de venta, pueda predecir el barrio al cual pertenece dicha casa. Este objetivo si bien no es generalizable, puesto que tiene validez sólo dentro de la localidad donde se encuentran las casas utilizadas para entrenar a los clasificadores, despierta nuestro interés y creemos podría utilizarse en una localidad dada para estudiar su entorno inmobiliario.
El porcentaje de predicción de barrios a partir de las características de las casas es muy alto, tanto para SVM como Ranfom Forest. Algunos barrios presentan mejores métricas con SVM y otros con Random Forest, mientras que otros barrios como Gilbert no resultan tan bien clasificados, lo que se podría deber a la variedad de viviendas en Gilbert u otra cosa. Queda pendiente revisitar estas métricas ya que como fué comentado en la presentación, son valores de predicción demasiado altos.
En cuanto a la clasificación de casas en grupos de casas, los resultados expuestos muestran que es posible realizar este análisis. Queda pendiente revisitar el problema con distintos parámetros a los 45 escogidos, y consultar con algún experto para validar la utilidad de los grupos generados, así como también medir la distancia entre clusters generados para obtener un número óptimo de clusters, ya que en el análisis presentado en este informe se escogieron de manera arbitraria "al ojo". Una vez determinada una relación entre grupos de casas establecidos por la sociedad y los grupos de casas generados por nuestro algoritmo, es posible obtener las características más relevantes para cada tipo de casa y así permitir a la gente enfocarse más en estos ya sea tanto para vender o comprar.
Finalmente, es importante mencionar la relevancia de las técnicas adquiridas durante el curso para poder realizar un proyecto de minería de datos desde cero. Efectivamente la mayor parte del tiempo se ocupa en entender cómo trabajar con los datos, qué normalizar, qué variables construir, qué eliminar y qué mantener, cómo transformar los datos a algún formato apropiado para análisis, etcétera, todo esto y más se aplicó para el desarrollo de este proyecto y de seguro se volverá a aplicar cuando sea necesario.